"""--------------------------------------------------------------------------------------------------
Filename:   mainapp.py
Author:     Ben Han (uha10@uclive.ac.nz)
Date:       17 May 2010
Description:
    (BI)ofeedback (S)trength & (SKI)ll (T)rainer - BISSKIT
    Biofeedback application for strength/skill training in Dysphagic patients.
    Main application module.
--------------------------------------------------------------------------------------------------"""

import sys, traceback
import wx
import wx.lib.newevent
import database
import numpy as np
from liveplot import LivePlot
from controls import ControlPanel
from dialogs import BreakDialog, StartDialog
from config import AppState, SessionState
from mainframe import MainFrame
from handlers import MainHandlers, PlotHandlers
from threading import RLock, Thread, Event
import Queue
import gc


# GUI id tags
ID_EXPORT_PATIENT = 51
ID_EXPORT_SESSION = 52
ID_CONFIGURATION = 53
ID_NEW_SESSION = 54
ID_STRENGTH = 1
ID_SKILL = 3


#===============================================================================
# CLASS: MainApp(wx.App)
#    Main application class for the biofeedback application.
#    Maintains ownership of the higher level objects during program execution
#===============================================================================
class MainApp(wx.App):
    def OnInit(self):
        wx.InitAllImageHandlers()
        print '==========================================================='
        print '    Main application started at:', wx.DateTime.Now().Format()
        print '==========================================================='
        
        # Display splash screen
        splash = StartDialog()
        splash.Show()
        
        self.status = AppState()                # state information for the application
        self.hdls = MainHandlers(self)          # event handlers for main window
        
        # Create the main frame
        self.main_frame = MainFrame(self, None, -1)
        self.SetTopWindow(self.main_frame)
        self.main_frame.Centre()
        self.main_frame.Show()
        
        self.hdls.frame = self.main_frame   # Pass the handler object a pointer to the main frame
        self.ctrl = None                    # Place holder for control panel window
        self.plot = None                    # Place holder for the live plot window
        self.console = None                 # Place holder for the serial Console object
        
        # Thread control objects
        self.pat_lock = RLock()             # must be acquired when modifying app.status.patient object
        self.sess_lock = RLock()            # must be acquired when modifying app.status.session object
        self.data_lock = RLock()            # must be acquired when modifying app.plot.x_data, app.plot.y_data arrays
        self.serial_loop_closed = Event()   # indicate state of the serial loop (flag set == serial loop not running)
        self.serial_loop_closed.set()       # will be cleared when serial reader loop is started
        self.serial_lock = RLock()          # used to ensure serial check is not executed while serial disconnect is in progress
        self.job_queue = Queue.Queue()      # contains jobs to be processed by the worker thread
        self.break_finished = Event()       # indicate state of break timer
        self.break_finished.set()           # Default state is set (not on break), this should be cleared 
                                            #     when the break timer (in break dialog) is running
        # Instantiate worker thread
        self.worker = Thread(target=self.__main_process, name='WorkerThread')
        self.worker.alive = True        # worker will loop while this variable is True
        self.worker.parse = True        # worker will process jobs only when this is True, else discards jobs
        self.worker.start()             # start worker thread
        
        # Create custom event
        self.BreakEvent, self.EVT_BREAK = wx.lib.newevent.NewEvent()
        self.Bind(self.EVT_BREAK, self._start_break)
        return 1

    def __main_process(self):
        # Repeatedly calls the job_queue.get() method. 
        # Block if the queue is empty (wait until a  job is added then grab it) 
        while self.worker.alive:
            try:
                job = self.job_queue.get(block=True)
                if self.worker.parse:
                    if job == None:     # indicates that the program is closing down
                        pass
                    else:
                        try:
                            job()
                        except:
                            print traceback.format_exc()
                else:   # discard jobs
                    pass
            except:
                print traceback.format_exc()
        
    def _start_break(self, event):
        # Create a breakdialog window and start the break counter
        print ">> Break started."
        try:
            self.break_frame = BreakDialog(self._on_end_break, self.cfg._break_len)
        except:
            print traceback.format_exc()

    def _on_end_break(self):
        # When the timer hits the set break time, end break
        print ">> Break ended."
        self.break_finished.set()
        self.sess_status.on_break = False
        try:
            self.console.flush()
        except:
            print traceback.format_exc()
        self.sess_status.alive = True
        self.plot.SetFocus()

    def display_status(self):
        mode = self.sess_status.mode
        s = ''
        if mode == 1:
            s = 'Strength mode    CALIBRATION'
        elif mode == 2:
            s = 'Strength mode    Trial: %i' % self.sess_status.trial_index
        elif mode == 3:
            s = 'Skill mode       CALIBRATION'
        elif mode == 4:
            s = 'Skill mode       Trial: %i' % self.sess_status.trial_index
        self.status_msg(s)
        
    def status_msg(self, msg):
        # Updates the status bar of the main frame
        self.main_frame.status_message(msg)
        if self.ctrl == None:
            pass
        else:
            self.ctrl.status_message(msg)

    def setup_training(self, type):
        # Called by the handler for the strength/skill buttons on the main menu
        self.sess_status = SessionState()   # state information for the current session
            
        # Disable automatic garbage collection. 
        # Garbage will be collected manually in between trials.
        gc.disable()
        gc.collect()
        
        typestr = ''
        if type == ID_STRENGTH:
            self.status_msg("Strength training mode. Calibration required.")
            self.sess_status.mode = 1       # Strength calibration
            typestr = 'Strength'
        elif type == ID_SKILL:
            self.status_msg("Skill training mode. Calibration required.")
            self.sess_status.mode = 3       # Skill calibration
            typestr = 'Skill'
        else:
            print 'Invalid option'
        
        if type != 1 and type != 3:
            print "Invalid training type"
            pass
        else:
            # Reset initial Y axis maximum
            self.cfg._y_axis_max = self.cfg._y_axis_max_init
            # Create a database.Session object to store data for the session
            self.sess_lock.acquire()    # ACQUIRE SESS_LOCK <--
            self.status.session = database.Session( config=self.cfg,
                                                    pid=self.status.patient.id,
                                                    type=typestr, 
                                                    blocks=[],
                                                    trials=[],
                                                    stats=None)
            self.sess_lock.release()    # RELEASE SESS_LOCK -->
            # Create a windows for control panel and plot
            self.phdls = PlotHandlers(self)     # event handlers for control panel
            self.ctrl = ControlPanel(self)      # control panel GUI
            self.plot = LivePlot(self)          # plotting class and GUI
            self.ctrl.Bind(wx.EVT_CLOSE, self.phdls.exit_plot)
            self.plot.Bind(wx.EVT_CLOSE, self.phdls.exit_plot)
            self.status.ctrl_frame = True
            self.status.plot_frame = True
            self.pat_lock.acquire()     # ACQUIRE PAT_LOCK <--
            title = typestr + ' training' + ' - ' + self.status.patient.name + '-' + self.status.patient.id
            self.pat_lock.release()     # RELEASE PAT_LOCK -->
            self.plot.SetTitle(title)
            self.main_frame.Hide()
            self.plot.Show()
            self.ctrl.Show()

    def parse_calib_data(self, data):
        # Process calibration data
        mode = self.sess_status.mode
        if mode == 2:       # Strength mode
            if len(data) < 5:
                sys.stderr.write('\n The calibration is incomplete. Array should contain five values.\n')
                self.on_str_button(wx.EVT_BUTTON)
            else:
                self.calib_data = np.array(data)
                self.sess_status.baseline = self.sess_status.str_target = \
                    self.calib_data.mean() * self.cfg.str_init_value/100.0
                self.status.session.calib_mean = self.calib_data.mean()    # store the mean calibration peak value
                self.status_msg('Baseline for strength target is:' + repr(self.sess_status.baseline))
                print '\nBaseline:', self.sess_status.baseline

        elif mode == 4:     # Skill mode
            if len(data) < 5:
                sys.stderr.write('\n The calibration is incomplete. Array should contain five values.\n')
                self.on_skl_button(wx.EVT_BUTTON)
            else:
                self.calib_data = np.array(data)
                self.sess_status.baseline = self.calib_data.mean()
                self.status.session.calib_mean = self.calib_data.mean()    # store the mean calibration peak value
                self.status_msg('Baseline for upper bound is:' + repr(self.sess_status.baseline))
                print '\nBaseline:', self.sess_status.baseline
                
                if self.cfg._use_delay:     # decrease the max x range to allow for delayed feedback
                    range_max_x = self.cfg._trial_len - self.cfg._x_margin - self.cfg._delay_len
                else:
                    range_max_x = self.cfg._trial_len - self.cfg._x_margin
                    
                range_min = [self.cfg._x_margin, self.cfg._lower_limit*self.sess_status.baseline]
                range_max = [range_max_x, self.cfg._upper_limit*self.sess_status.baseline]
                self.plot.tfactory.set_range_min(range_min)
                self.plot.tfactory.set_range_max(range_max)
                self.sess_status.min_size = [1, 10]
                self.sess_status.max_size = [self.sess_status.max_range[0] - self.sess_status.min_range[0], 
                                         self.sess_status.max_range[1] - self.sess_status.min_range[1]]
        
    def end_block(self):
        # Report the results of the last block of trials
        print '>> End Block'

        # Create a database.Block object, add current emgdata and history
        self.block = database.Block(self.sess_status.history)
        self.block.calc()                               # calculate block stats
        self.sess_lock.acquire()    # ACQUIRE SESS_LOCK <--
        self.status.session.add_block(self.block)       # add block to database.Sessions object
        self.sess_lock.release()    # RELEASE SESS_LOCK -->
        self.sess_status.history = []       # reset history for the next block
        self.sess_status.plots_off = 0      # reset fading counters
        self.sess_status.plots_on = 0
        self.sess_status.fading_phase = False

        # Start a break if this is not the last block
        if self.sess_status.trial_index < self.cfg._trials_per_session:
            try:
                # This method should be called from the worker thread
                self.break_finished.clear()     # indicate that the break has started
                break_event = self.BreakEvent()
                wx.PostEvent(self, break_event)
            except:
                print traceback.format_exc()

    def end_session(self):
        # Housekeeping at the end of the session, all trials completed
        self.worker.parse = False           # discard remaining jobs in queue
        dlg = wx.MessageDialog(None, "Session finished!", "Session complete", wx.OK | wx.ICON_INFORMATION)
        dlg.ShowModal()
        sys.stdout.write('>>> End Session\n')
        try:
            self.phdls.save_sess(None)
        except:
            sys.stdout.write(traceback.format_exc())
            
        self.cfg._y_axis_max = 200              # reset plot y axis max for the next session
        self.phdls.serial_disconnect(None)      # disconnect serial port
        self.serial_loop_closed.wait()       # wait for serial loop to finish before destroying plot object in restore_mainmenu()
        self.status.session = None      # reset reference to current session object
        self.sess_status = None         # reset reference to current session state object
        self.restore_mainmenu()
        
    def restore_mainmenu(self):
        # Restores the main menu. 
        # Called at the end of the handler for <Main Menu> button in the control panel
        # Enable automatic garbage collection. 
        gc.enable()
        try:
            self.plot.Destroy()                     # close plot frame
            self.plot = None                        # set plot frame pointer to None
            self.status.plot_frame = False          # set plot_frame status to False 
            self.ctrl.Destroy()                     # close control panel
            self.ctrl = None                        # set control panel pointer to None
            self.status.ctrl_frame = False          # set ctrl_frame status to False
            
            self.main_frame.update_patient_panel()  # update to show the completed session
            self.status_msg('Main menu')
            self.main_frame.Show()
            self.main_frame.SetFocus()
        except:
            print traceback.format_exc()
        
    def on_exit(self, event):
        # Shut down the application.
        #    - Check that the current patient's data is saved
        #    - Ensure all threads are finished
        #    - Destroy all windows
        
        if self.status.patient == None:
            pass
        elif self.status.patient_changed:
            dialog_text = "Save patient file?"
            dial = wx.MessageDialog(None, dialog_text, 'Question',
                                    wx.YES_NO | wx.YES_DEFAULT | wx.ICON_QUESTION)
            ret = dial.ShowModal()
            if ret == wx.ID_YES:
                self.hdls.save_patient(None)
        
        # Kill all processes
        self.worker.alive = False
        self.job_queue.put(None)
        self.data_lock.acquire()
        self.main_frame.Destroy()
        self.data_lock.release()
        print '==========================================================='
        print '    Main application closed at: ', wx.DateTime.Now().Format()
        print '==========================================================='
        
# end of class MainApp

if __name__ == "__main__":
    MyApp = MainApp(0)
    MyApp.MainLoop()

# End of mainapp.py